﻿using NAudio.Vorbis;
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;

namespace 无缝播放器
{

    /// <summary>
    /// 音频流类
    /// </summary>
    public class 音频流 : IWaveProvider
    {

        #region 全局变量
        //全局初始化量
        int 本类线程号 = -1;
        public 音频信息 信息 = null;
        public 器材 本类器材 = null;
        List<string> 待播放文件组 = null;
        public List<WaveChannel32> 读取器组 = null;
        public List<VorbisWaveReader> obb读取器组 = null;
        public List<MemoryStream> 内存文件组 = null;
        public List<WaveOutEvent> 播放器组 = null;
        public WaveOut 过渡播放器 = null;
        public AudioFileReader 过渡文件音频文件读取器 = null;

        public TextBlock 信息栏 = null;
        public 流 本类流;
        public string 过渡音频 = null;
        public bool 强制返0 = false;

        //旧式
        public IWaveProvider 音频 = null;
        bool 等待Read释放 = false;
        int 传入次数 = 0;
        bool 停止读入 = false;
        #endregion
        #region 构造函数
        /// <summary>
        /// 构造函数，在创建类时申请读入源音频
        /// 可以直接调用this来播放数据，但是，你需要在另一个WaveFormat函数中指定要播放的文件，否则，Read不会执行。
        /// </summary>
        /// <param name="音频"></param>
        /// <param name="静音"></param>
        public void 读入音频流(List<string> 传入文件组, 器材 器材, 音频信息 传入信息, 流 流列表, TextBlock 传入, bool 过渡段运行 = false, bool 静音 = false)
        {

            信息栏 = 传入;
            本类流 = 流列表;
            信息 = 传入信息;
            本类线程号 = 信息.线程号;
            //Console.WriteLine($"{本类线程号} - 新建 音频状态 {信息.播放状态}");

            //设置全局器材信息
            本类器材 = 器材;
            待播放文件组 = 传入文件组;
            读取器组 = 本类器材.读取器组;
            obb读取器组 = 本类器材.obb读取器组;
            内存文件组 = 本类器材.内存文件组;
            播放器组 = 本类器材.播放器组;
            过渡播放器 = 本类器材.过渡播放器;
            过渡文件音频文件读取器 = 本类器材.过渡文件音频文件读取器;
            过渡音频 = 本类器材.过渡音频;
            //旧式
            //音频 = 传入音频;
            //使用reader = 该reader;



            播放器组 = 本类器材.播放器组;
            待播放文件组 = 传入文件组;
            foreach (string i in 传入文件组)
            {
                // Debug.WriteLine("读取中");
                信息.淡入_字节修改量 = 0;
                if (过渡段运行 == false)
                {
                    读取文件到内存(i);
                }
            }
            //初始化第一个文件 

            设定下一文件();
            //  Console.WriteLine("设置格式");
            WaveFormat = 读取器组[0].WaveFormat;
            信息.每采样字节 = WaveFormat.BlockAlign;

            本类器材.播放器组[信息.线程号].Init(this);



            本类器材.播放器组[信息.线程号].Play();
        }
        #endregion
        #region 接口必要函数

        /// <summary>
        ///  接口必须实现的函数，当播放器init后，NAudio将自动调用此方法来获取波形数据，当读入0时将停止
        /// </summary>
        /// <param name="缓冲区">NAudio内部的缓冲区</param>
        /// <param name="起始位置">NAudio中设定音频播放的起始位置，需要输入字节数</param>
        /// <param name="读取大小">指示要将  缓冲区  从  起始位置  读取多大的音频，最大  不可超过  缓冲区  的  最大长度</param>
        /// <returns>返回此次读取的波形大小</returns>
        public int Read(byte[] 缓冲区, int 起始位置, int 读取大小)
        {

            if (等待Read释放)
            {
                return 0;
            }

            if (信息.当前拍 + 1 != 信息.当前段拍数)
            {
                信息.切换点在末拍 = false;
            }

            lock (this)
            {
                //如果已经禁止读入，就直接停掉输出
                if (信息.仍需读取流 == false)
                {
                    播放器组[信息.线程号].Stop();
                    等待Read释放 = true;
                    信息.仍需读取流 = true;
                    return 0;
                }
                //用于计算当前偏差


                //开始主要播放逻辑
                for (int i = 0; i < 读取大小;)
                {

                    //如果该流已移交到新的流，播放完剩余流后停止输出
                    if (停止读入)
                    {
                        i += 读取器组[信息.当前段编号 - 1].Read(缓冲区, 起始位置 + i, 读取大小 - i);
                        淡出流(缓冲区, 起始位置, 读取大小, 1);
                        Debug.WriteLine($"该流已移交到新的流，播放完剩余流后停止输出");

                        //内部控制，音量降完后直接关闭输出
                        if (强制返0)
                        {
                            读取大小 = 0;
                        }

                        return 读取大小;
                    }

                    //更新当前音乐小节/时长信息


                    double 已播放字节 = 本类器材.播放器组[信息.线程号].GetPosition() - 信息.偏移 + 读取大小;
                    信息.当前播放位置 = (本类器材.播放器组[信息.线程号].GetPosition() - 信息.偏移) * 1000.0 / 读取器组[信息.当前段编号].WaveFormat.BitsPerSample / 读取器组[信息.当前段编号].WaveFormat.Channels * 8.0 / 读取器组[信息.当前段编号].WaveFormat.SampleRate;
                    double 每拍时长 = (double)60 * (double)1000 / (double)信息.当前段BPM;
                    信息.每拍毫秒 = Convert.ToInt32(Math.Floor(每拍时长));
                    double 每读取时毫秒数 = (读取大小) * 1000.0 / 读取器组[信息.当前段编号].WaveFormat.BitsPerSample / 读取器组[信息.当前段编号].WaveFormat.Channels * 8.0 / 读取器组[信息.当前段编号].WaveFormat.SampleRate;
                    double 每毫秒bit = 读取大小 / 每读取时毫秒数;
                    信息.每拍Bit = 每拍时长 * 每毫秒bit;
                    int 当前拍 = (int)Math.Floor((double)已播放字节 / (double)信息.每拍Bit);
                    信息.当前小节 = (int)Math.Floor((double)当前拍 / (double)信息.当前段拍数);
                    信息.当前拍 = 当前拍 % 信息.当前段拍数;
                    double 下一拍的字节 = 信息.每拍Bit * (当前拍 + 1);
                    //Console.WriteLine($"当前{信息.当前小节}小节{信息.当前拍 + 1}/{信息.当前段拍数}拍");
                    //Console.WriteLine($"读取器大小{读取器组.Count} 当前访问{信息.当前段编号} 当前位置{信息.当前播放位置} 传入缓冲{缓冲区.Length}");
                    //如果已经禁止读入，就直接停掉输出
                    if (信息.仍需读取流 == false)
                    {
                        播放器组[信息.线程号].Stop();
                        等待Read释放 = true;
                        信息.仍需读取流 = true;
                        return 0;
                    }
                    //无缝循环逻辑
                    ///判断当前播放是否快要结尾
                    if (读取器组[信息.当前段编号].Length - 读取器组[信息.当前段编号].Position <= 读取大小 - i)
                    {

                        // Console.WriteLine($"因为剩余的字节数{读取器组[信息.当前段编号].Length - 读取器组[信息.当前段编号].Position}>={读取大小 - i}");
                        // Console.WriteLine($"所以将文件从 {起始位置 + i} 向下读余下的 {(int)读取器组[信息.当前段编号].Length - (int)读取器组[信息.当前段编号].Position}字节");

                        i += 读取器组[信息.当前段编号].Read(缓冲区, 起始位置 + i, (int)读取器组[信息.当前段编号].Length - (int)读取器组[信息.当前段编号].Position);
                        if (信息.当前段编号 == 信息.下一段编号)
                        {
                            信息.循环次数++;
                        }

                        信息.偏移 += (long)本类器材.读取器组[信息.当前段编号].Length;
                        Console.WriteLine(信息.偏移);
                        Console.WriteLine(读取器组[信息.当前段编号].Position);

                        设定下一文件(true);
                        读取器组[信息.当前段编号] = 读取器组[信息.下一段编号];

                        读取器组[信息.当前段编号].Position = 0;

                        /*
                         * 已丢弃 因为开始已判断好
                        if (设置停止)
                        {
                            控制的播放器.Stop();

                            传出_循环次数 = 0;
                            return 0;
                        }
                        */

                    }
                    //交互衔接逻辑

                    ///判断是否现在是交互式无缝循环模式
                    if (信息.播放下一拍后跳转 && 信息.循环方法 == 音频信息.循环类型.交互式无缝循环)
                    {
                        ///若超量则正常读取并继续（即使完成了下一次读取也没到下一拍结束）
                        已播放字节 = 本类器材.播放器组[信息.线程号].GetPosition() + 读取大小; //再确定一下时间，增加精度
                        if (下一拍的字节 - 已播放字节 > 读取大小)
                        {
                            Debug.WriteLine($"即使完成了下一次读取也没到下一拍结束 {下一拍的字节 - 已播放字节} > {读取大小}?");
                            i += 读取器组[信息.当前段编号].Read(缓冲区, 起始位置 + i, 读取大小 - i);
                            设定下一文件();
                            Console.WriteLine("!");
                            return 读取大小;
                        }
                        else
                        {
                            //若未超量（本拍完成前就要进入新的小节）
                            /*
                            if (信息.当前拍 + 1 == 信息.当前段拍数 - 2)
                            {
                             
                                if (信息.等待效果音 && 信息.有过渡音频)
                                {
                                    过渡播放器.Play();
                                    信息.等待效果音 = false;
                                }
                                Debug.WriteLine($"{信息.当前拍 + 1}/{信息.当前段拍数 - 2}  还剩两拍");
                                i += 读取器组[信息.当前段编号].Read(缓冲区, 起始位置 + i, 读取大小 - i);
                                设定下一文件();
                      
                                return 读取大小;
                            }
                            Debug.WriteLine($"{下一拍的字节 - 已播放字节} < {读取大小}?");
                            */

                            ///若小节数还远，则再读一次

                            if (信息.当前拍 + 1 != 信息.当前段拍数 || 信息.切换点在末拍 == true)
                            {
                                Console.WriteLine("!");
                                Debug.WriteLine($"{信息.当前拍} 举例过渡还有数拍，继续寻拍");
                                i += 读取器组[信息.当前段编号].Read(缓冲区, 起始位置 + i, 读取大小 - i);

                                设定下一文件();
                                return 读取大小;
                            }
                            if (信息.有过渡音频)
                            {
                                过渡播放器.Play();
                            }
                            //Console.WriteLine($"在{信息.当前小节}小节{信息.当前拍+1}/{信息.当前段拍数}拍时切换");
                            淡出流(缓冲区, 起始位置 + i, 读取大小 - i, 1);
                            i += 读取器组[信息.当前段编号].Read(缓冲区, 起始位置 + i, 读取大小 - i);
                            /*
                            信息.播放下一拍后跳转 = false;
                            设定下一文件(true, true);
                            读取器组[信息.当前段编号].Position = 0;
                            */
                            停止读入 = true;
                            //Console.WriteLine($"{信息.线程号}-出流");
                            WaveOutEvent waveOut = new WaveOutEvent();
                            本类器材.播放器组.Add(waveOut);
                            音频流 文件 = new 音频流(); //已经无构造方法 记得修改
                            本类流.文件.Add(文件);
                            信息.偏移 = 0;
                            信息.线程号++;
                            信息.当前段编号++;
                            信息.播放下一拍后跳转 = false;

                            文件.读入音频流(待播放文件组, 本类器材, 信息, 本类流, 信息栏, true);
                            return 读取大小;
                            /* 稍后用于新节的代码
                               if (传入次数 == 25)
                               {
                                   //改进的方法，设置一个自定义的锁，禁止再访问即可
                                   停止读入 = true;
                                   Console.WriteLine($"{信息.线程号}-出流");
                                   WaveOutEvent waveOut = new WaveOutEvent();
                                   器材.播放器组.Add(waveOut);
                                   音频流 文件 = new 音频流(待播放文件组, 播放器组, 信息); //已经无构造方法 记得修改
                                   return 读取大小;
                               }

                               传入次数++;
                  */


                        }

                    }
                    //正常读取   当源音频流被Read，流就会被读到播放器
                    //如果已经禁止读入，就直接停掉输出
                    if (信息.仍需读取流 == false)
                    {
                        播放器组[信息.线程号].Stop();
                        等待Read释放 = true;
                        信息.仍需读取流 = true;
                        return 0;
                    }
                    i += 读取器组[信息.当前段编号].Read(缓冲区, 起始位置 + i, 读取大小 - i);
                    设定下一文件();
                    if (信息.播放状态 == 音频信息.音频状态.播放中 || 信息.当前UI绑定线程 != 信息.线程号)
                    {


                        //    Console.WriteLine($"{ 信息.当前UI绑定线程 } / {信息.线程号}尝试淡入{信息.播放状态}/{信息.淡入_字节修改量}");
                        淡入流(缓冲区, 起始位置, 读取大小, 1);
                    }


                }

            }
            return 读取大小;

        }


        /// <summary>
        /// 接口必须实现的函数，获取当前播放流的类型
        /// 修改此处或许决定播放的文件
        /// </summary>
        public WaveFormat WaveFormat { get; private set; }
        #endregion
        #region 读取相关函数
        /// <summary>
        /// 将传入的ogg文件读到内存中
        /// </summary>
        /// <param name="需读取的文件名"></param>
        public void 读取文件到内存(string 需读取的文件名)
        {
            try
            {
                //新建一个内存文件
                内存文件组.Add(new MemoryStream());
                //新建一个文件流并载入需要读取的文件

                FileStream 文件流 = new FileStream(需读取的文件名, FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read);
                文件流.CopyTo(内存文件组[内存文件组.Count - 1]);
                //从文件读取器中载入需要读取的文件
                obb读取器组.Add(new VorbisWaveReader(内存文件组[内存文件组.Count - 1]));
                文件流.Close();
                文件流.Dispose();
                读取器组.Add(new WaveChannel32(obb读取器组[内存文件组.Count - 1]));
            }
            catch
            {
                MessageBox.Show("在读取文件的时候出现问题，这可能是因为\r\n1 . 您已经移动、删除、占用了文件\r\n2. 该文件的格式不正确，不能被支持\r\n若您已经解决了该问题，请停止播放后再试一次", "发生读取文件流错误");
                return;
            }

            // Console.WriteLine($"读入文件{需读取的文件名}完成，当前文件标识符 {内存文件组.Count - 1}");
            // Debug.WriteLine($"读入文件{需读取的文件名}完成，当前文件标识符 {内存文件组.Count - 1}");
        }
        /// <summary>
        /// 设置下一个要播放的文件
        /// </summary>
        /// <param name="准备播放下一段落"></param>
        /// <param name="跳过本循环段"></param>
        /// <param name="手动按键"></param>
        public void 设定下一文件(bool 准备播放下一段落 = false, bool 跳过本循环段 = false, bool 手动按键 = false)
        {
            //状态显示

            // Console.WriteLine($"{本类线程号}- {信息.当前段编号}>{信息.下一段编号 }");
            //自动交互
            ///判断是否为第一次播放，如果是，初始化一次数值
            if (信息.播放状态 == 音频信息.音频状态.未定义)
            {
                // Console.WriteLine("当前是未定义状态");
                信息.当前段编号 = 0;
                信息.播放状态 = 音频信息.音频状态.播放中;
            }
            ///设定节拍文件信息
            if (信息.节拍特例.Count > 1)
            {
                信息.当前段拍数 = Convert.ToInt32(信息.节拍特例[信息.当前段编号].ToString());
            }
            else
            {
                信息.当前段拍数 = 4;
            }
            ///判断该文件是否需要循环，如果是，则设置本次和下次播放编号相同，若不需要循环，则查看是否有下一个文件，有则准备播放下一个文件，没有则停止。
            if (信息.循环文件下标.Contains(信息.当前段编号))
            {
                信息.下一段编号 = 信息.当前段编号;
            }
            else
            {
                ///若不需要循环，则查看是否有下一个文件，有则准备播放下一个文件，没有则停止。
                if (读取器组.Count - 1 == 信息.当前段编号)
                {
                    信息.播完停止 = true;

                }
                else
                {
                    信息.下一段编号 = 信息.当前段编号 + 1;
                }
            }


            //手动交互
            /// 若还有下一个段落，且用户按键，则在播放下一拍后进行跳转 ！将return
            if (读取器组.Count - 1 != 信息.当前段编号 && 信息.播放下一拍后跳转 == false && 准备播放下一段落 == true && 跳过本循环段 == true && 手动按键 == true)
            {
                信息.播放下一拍后跳转 = true;
                return;
            }
            ///因为已经按键，如果是交互式无缝循环，那么就跳过处理！将return
            if (信息.播放下一拍后跳转 && 信息.循环方法 == 音频信息.循环类型.交互式无缝循环)
            {
                Debug.WriteLine("等待跳转");
                return;
            }

            //程序交互 - 可能有BUG
            if (准备播放下一段落 == true)
            {

                //如果是一个循环中，查看用户是否准备跳过本次循环，如果是且有下一个文件，则跳过
                if (读取器组.Count - 1 != 信息.当前段编号 && 跳过本循环段 == true && 信息.当前段编号 == 信息.下一段编号)
                {
                    信息.下一段编号++;
                    信息.当前段编号 = 信息.下一段编号;

                    return;
                }
                //如果是一个循环中，查看用户是否准备跳过本次循环，如果是但没有下一个文件，则此次播放完毕后停止播放
                if (读取器组.Count - 1 == 信息.当前段编号 && 跳过本循环段 == true && 信息.当前段编号 == 信息.下一段编号)
                {

                    信息.播完停止 = true;
                    return;

                }

                信息.当前段编号 = 信息.下一段编号;

                return;
            }

        }
        /// <summary>
        ///  设置过渡时要播放的转场文件
        /// </summary>
        /// <param name="需要用于过渡的音频文件"></param>
        public void 设置过渡文件(string 需要用于过渡的音频文件)
        {
            if (File.Exists(需要用于过渡的音频文件))
            {
                过渡音频 = 需要用于过渡的音频文件;
                过渡文件音频文件读取器 = new AudioFileReader(需要用于过渡的音频文件);

                过渡播放器.Init(过渡文件音频文件读取器);

            }
        }
        #endregion
        #region 特效函数
        public void 淡出流(byte[] 缓冲流, int 流开始点, int 流结束点, float 拍数)
        {

            if (信息.已在淡出 || 强制返0)
            {
                return;
            }

            unsafe
            {
                信息.已在淡出 = true;
                float 左声道A, 右声道A, 左声道B, 右声道B;
                fixed (byte* 缓冲区指针 = 缓冲流)
                {
                    fixed (byte* 临时缓冲区指针 = 缓冲流)
                    {
                        float* 缓冲区数据A = (float*)(缓冲区指针 + 流开始点);
                        float* 缓冲区数据B = (float*)临时缓冲区指针;
                        int 流终点 = 流结束点 / 信息.每采样字节 * 2;
                        for (int i = 0; i < 流终点; i += 2)
                        {
                            float 音量倍率 = (float)1 - (float)信息.淡出_字节修改量 / ((float)信息.每拍Bit * (float)0.25 * 拍数);

                            if (音量倍率 <= 0.01)
                            {

                                音量倍率 = 0.0f;
                                强制返0 = true;

                            }

                            左声道A = 缓冲区数据A[i] * 音量倍率;
                            右声道A = 缓冲区数据A[i + 1] * 音量倍率;
                            左声道B = 缓冲区数据B[i] * 音量倍率;
                            右声道B = 缓冲区数据B[i + 1] * 音量倍率;


                            缓冲区数据A[i] = 左声道A;
                            缓冲区数据B[i + 1] = 右声道B;
                            信息.淡出_字节修改量 += 2;

                        }
                    }
                }

            }
            信息.已在淡出 = false;

        }
        public void 淡入流(byte[] 缓冲流, int 流开始点, int 流结束点, float 拍数)
        {


            if (信息.已在淡入)
            {
                //  Console.WriteLine("淡入被拒绝");
                return;
            }
            unsafe
            {
                信息.已在淡入 = true;
                float 左声道A, 右声道A, 左声道B, 右声道B;
                fixed (byte* 缓冲区指针 = 缓冲流)
                {
                    fixed (byte* 临时缓冲区指针 = 缓冲流)
                    {
                        float* 缓冲区数据A = (float*)(缓冲区指针 + 流开始点);
                        float* 缓冲区数据B = (float*)临时缓冲区指针;
                        int 流终点 = 流结束点 / 信息.每采样字节 * 2;
                       
                        for (int i = 0; i < 流终点; i += 2)
                        {
                            float 音量倍率 = (float)信息.淡入_字节修改量 / ((float)信息.每拍Bit * (float)0.25 * 拍数);

                            if (音量倍率 >= 1)
                            {
                                信息.播放状态 = 音频信息.音频状态.满音量;
                                信息.当前UI绑定线程 = 信息.线程号;
                                信息.已在淡入 = false;
                                return;
                            }

                            左声道A = 缓冲区数据A[i] * 音量倍率;
                            右声道A = 缓冲区数据A[i + 1] * 音量倍率;
                            左声道B = 缓冲区数据B[i] * 音量倍率;
                            右声道B = 缓冲区数据B[i + 1] * 音量倍率;


                            缓冲区数据A[i] = 左声道A;
                            缓冲区数据B[i + 1] = 右声道B;
                            信息.淡入_字节修改量 += 2;

                        }
                    }
                }

            }
            信息.已在淡入 = false;
        }
        #endregion

        ~音频流()
        {
            Console.WriteLine("音频播放类已析构");

        }

    }

}
